﻿using System;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Xml;
using NUnit.Framework;
using PropertyExpression.Common;
using PropertyExpression.ControllableQuery.Test.Queries.T4;
using PropertyExpression.ControllableQuery.Test.Queries.StringBuilding;
using PropertyExpression.ControllableQuery.Toolkit;
using ModelPublicationSearchQuery = PropertyExpression.ControllableQuery.Test.Queries.T4.ModelPublicationSearchQuery;


namespace PropertyExpression.ControllableQuery.Test.QueryCheck
{
    [TestFixture]
    public class MainTest
    {
        [Test]
        public void ModelPublicationSearchQuery()
        {
            CheckQueries(
                ConnectionString,
                queryContext => new ModelPublicationSearchQuery(
                                    queryContext.CreateParam<string>().AsOptionalValue(),
                                    OptionalValue.Nothing<IParam<string>>(),
                                    OptionalValue.Nothing<IParam<string>>()
                                    ),
                queryContext => new ModelPublicationSearchQuery(
                                    queryContext.CreateParam<string>().AsOptionalValue(),
                                    queryContext.CreateParam<string>().AsOptionalValue(),
                                    queryContext.CreateParam<string>().AsOptionalValue()
                                    ),
                queryContext => new ModelPublicationSearchQuery(
                                    OptionalValue.Nothing<IParam<string>>(),
                                    OptionalValue.Nothing<IParam<string>>(),
                                    OptionalValue.Nothing<IParam<string>>()
                                    )
                );
        }

        [Test]
        public void PartLinkQuery()
        {
            CheckQueries(
                ConnectionString,
                queryContext => new PartLinkQuery(
                                    queryContext.CreateParam<Guid>(),
                                    queryContext.CreateParam<Guid>(),
                                    queryContext.CreateParam<Guid>(),
                                    new WithInnerPartLinkSwitcher(
                                        true,
                                        queryContext.CreateParam<string>(),
                                        queryContext.CreateParam<Guid>())
                                    )
                );
        }

        [Test]
        public void TableValuedParameterQuery()
        {
            CheckQueries(
                ConnectionString,
                queryContext => new TableValuedParameterQuery(
                                    queryContext.CreateTableValuedParam<IPublicationCodeType>()
                                    )
                );
        }

        [Test]
        public void StringBuildingModelPublicationSearchQuery()
        {
            CheckQueries(
                ConnectionString,
                queryContext => new Queries.StringBuilding.ModelPublicationSearchQuery(
                                    queryContext.CreateParam<string>().AsOptionalValue(),
                                    OptionalValue.Nothing<IParam<string>>(),
                                    OptionalValue.Nothing<IParam<string>>()
                                    ),
                queryContext => new Queries.StringBuilding.ModelPublicationSearchQuery(
                                    queryContext.CreateParam<string>().AsOptionalValue(),
                                    queryContext.CreateParam<string>().AsOptionalValue(),
                                    queryContext.CreateParam<string>().AsOptionalValue()
                                    ),
                queryContext => new Queries.StringBuilding.ModelPublicationSearchQuery(
                                    OptionalValue.Nothing<IParam<string>>(),
                                    OptionalValue.Nothing<IParam<string>>(),
                                    OptionalValue.Nothing<IParam<string>>()
                                    )
                );
        }

        [Test]
        public void StringBuildingModelPublicationQuery()
        {
            CheckQueries(
                ConnectionString,
                queryContext => new ModelPublicationQuery(
                                    queryContext.CreateParam<string>(),
                                    queryContext.CreateParam<string>()
                                    )
                );
        }

        [Test]
        public void StringBuildingModelPublicationQuery2()
        {
            CheckQueries(
                ConnectionString,
                queryContext => new ModelPublicationQuery2(
                                    queryContext.CreateParam<string>(),
                                    queryContext.CreateParam<string>()
                                    )
                );
        }

        [Test]
        public void PublicationTypeInsertQuery()
        {
            CheckNonQueries(
                ConnectionString,
                queryContext => new PublicationTypeInsertQuery(
                                    queryContext.CreateParam<Guid>(),
                                    queryContext.CreateParam<int>(),
                                    queryContext.CreateParam<NVarChar<L4000>>(),
                                    queryContext.CreateParam<Decimal<P18, S7>>(),
                                    queryContext.CreateParam<Money>()
                                    )
                );
        }

        [Test]
        public void PublicationTypeUpdateQuery()
        {
            CheckNonQueries(
                ConnectionString,
                queryContext => new PublicationTypeUpdateQuery(
                                    queryContext.CreateParam<Guid>(),
                                    queryContext.CreateParam<IOptionalValue<NVarChar<L4000>>>()
                                    )
                );
        }

        [Test]
        public void PublicationTypeSelectQuery()
        {
            CheckQueries(
                ConnectionString,
                queryContext => new PublicationTypeSelectQuery(queryContext.CreateParam<Guid>())
                );
        }

        [Test]
        public void PublicationTypeDeleteQuery()
        {
            CheckNonQueries(
                ConnectionString,
                queryContext => new PublicationTypeDeleteQuery(
                                    queryContext.CreateParam<Guid>()
                                    )
                );
        }

        [Test]
        public void ModelPublicationQuery()
        {
            CheckQueries(
                ConnectionString,
                queryContext => Query.ModelPublicationQuery(
                    queryContext.CreateParam<string>(),
                    queryContext.CreateParam<string>())
                );
        }

        private static void CheckQueries<T>(string connectionString, params Func<CheckQueryContext, IQuery<T>>[] queryFuncs)
        {
            QueryChecker.CheckQueries(connectionString, ParamTypeChecker, QueryResultPropertyTypeChecker, queryFuncs);
        }

        public static void CheckNonQueries(string connectionString, params Func<CheckQueryContext, INonQuery>[] queryFuncs)
        {
            QueryChecker.CheckNonQueries(connectionString, ParamTypeChecker, queryFuncs);
        }

        [Test]
        public void CheckAllQueries()
        {
            QueryChecker.CheckAllQueriesInAvailableAssemblies(
                ConnectionString,
                ParamTypeChecker,
                QueryResultPropertyTypeChecker,
                ParamCreator,
                type => true,
                delegate { throw new ApplicationException(); }
                );
        }

        public static ParamCreator ParamCreator
        {
            get
            {
                return (type, queryContext) =>
                           {
                               var paramType = type.GetGenericArguments().Single();
                               if (paramType.IsGenericTypeAndEquals(typeof (ITableValued<>)))
                               {
                                   return createTableValuedParamMethodInfo.MakeGenericMethod(
                                       paramType.GetGenericArguments())
                                       .Invoke(queryContext, new object[] {});
                               }
                               else
                               {
                                   return createParamMethodInfo.MakeGenericMethod(paramType)
                                       .Invoke(queryContext, new object[] {});
                               }
                           };
            }
        }

        private static readonly MethodInfo createParamMethodInfo = 
            Toolkit.Util.GetMethodInfo<Func<CheckQueryContext, IParam<object>>>(_ => _.CreateParam<object>()).GetGenericMethodDefinition();

        private static readonly MethodInfo createTableValuedParamMethodInfo =
            Toolkit.Util.GetMethodInfo<Func<CheckQueryContext, IParam<ITableValued<object>>>>(_ => _.CreateTableValuedParam<object>()).GetGenericMethodDefinition();

        public static ParamTypeChecker ParamTypeChecker
        {
            get
            {
                return (sqlParamInfo, queryText, queryType, xmlDocument, xmlNamespaceManager) =>
                {
                    if (sqlParamInfo.SqlType.CompareSqlTypes("nvarchar(max)"))
                    {
                        CheckParameterType<string>(sqlParamInfo, queryText, queryType);
                    }
                    else if (sqlParamInfo.SqlType.CompareSqlTypes("nvarchar"))
                    {
                        CheckNVarCharParameterType(sqlParamInfo, queryText, queryType, xmlDocument, xmlNamespaceManager);
                    }
                    else if (sqlParamInfo.SqlType.CompareSqlTypes("decimal"))
                    {
                        CheckDecimalParameterType(sqlParamInfo, queryText, queryType, xmlDocument, xmlNamespaceManager);
                    }
                    else if (sqlParamInfo.SqlType.CompareSqlTypes("money"))
                    {
                        CheckParameterType<Money>(sqlParamInfo, queryText, queryType);
                    }
                    else if (sqlParamInfo.SqlType.CompareSqlTypes("uniqueidentifier"))
                    {
                        CheckParameterType<Guid>(sqlParamInfo, queryText, queryType);
                    }
                    else if (sqlParamInfo.SqlType.CompareSqlTypes("int"))
                    {
                        CheckParameterType<int>(sqlParamInfo, queryText, queryType);
                    }
                    else
                    {
                        throw new InvalidOperationException();
                    }
                };
            }
        }

        private static void CheckDecimalParameterType(
            SqlParamInfo sqlParamInfo, string queryText, Type queryType,
            XmlDocument xmlDocument, XmlNamespaceManager xmlNamespaceManager)
        {
            var paramType = sqlParamInfo.ParamInfo.ParamType;
            if (paramType.IsGenericTypeAndEquals(typeof(Decimal<,>))
                && paramType.GetGenericArguments()[0].GetPrecision() ==
                   sqlParamInfo.GetPrecision(xmlDocument, xmlNamespaceManager)
                && paramType.GetGenericArguments()[1].GetScale() ==
                sqlParamInfo.GetScale(xmlDocument, xmlNamespaceManager))
            {
                //no-op
            }
            else if (paramType.IsGenericTypeAndEquals(typeof(IOptionalValue<>))
                     && paramType.GetGenericArguments().Single().IsGenericTypeAndEquals(typeof(Decimal<,>))
                     && paramType.GetGenericArguments().Single().GetGenericArguments()[0].GetPrecision() ==
                        sqlParamInfo.GetPrecision(xmlDocument, xmlNamespaceManager)
                     && paramType.GetGenericArguments().Single().GetGenericArguments()[1].GetScale() ==
                        sqlParamInfo.GetScale(xmlDocument, xmlNamespaceManager))
            {
                //no-op
            }
            else
            {
                throw new QueryCheckException(
                    string.Format(
                        @"The type of parameter '{0}' was incorrect. Expected SQL Server Database Engine type DataType = '{1}', Precision = '{5}', Scale = '{6}' but was .NET Framework type '{2}'.
QueryType = '{4}'
QueryText = 
{3}",
                        sqlParamInfo.ParamInfo.Name,
                        sqlParamInfo.SqlType,
                        paramType,
                        queryText,
                        queryType,
                        sqlParamInfo.GetPrecision(xmlDocument, xmlNamespaceManager),
                        sqlParamInfo.GetScale(xmlDocument, xmlNamespaceManager)));
            }
            Console.WriteLine("{0} {1} {2} {3} {4}",
                              sqlParamInfo.ParamInfo.Name,
                              sqlParamInfo.ParamInfo.ParamType,
                              sqlParamInfo.SqlType,
                              sqlParamInfo.GetPrecision(xmlDocument, xmlNamespaceManager),
                              sqlParamInfo.GetScale(xmlDocument, xmlNamespaceManager));
        }

        private static void CheckNVarCharParameterType(
            SqlParamInfo sqlParamInfo, string queryText, Type queryType,
            XmlDocument xmlDocument, XmlNamespaceManager xmlNamespaceManager)
        {
            var paramType = sqlParamInfo.ParamInfo.ParamType;
            if (paramType.IsGenericTypeAndEquals(typeof(NVarChar<>))
                && paramType.GetGenericArguments().Single().GetLength() ==
                   sqlParamInfo.GetNVarCharLength(xmlDocument, xmlNamespaceManager))
            {
                //no-op
            }
            else if (paramType.IsGenericTypeAndEquals(typeof(IOptionalValue<>))
                     && paramType.GetGenericArguments().Single().IsGenericTypeAndEquals(typeof(NVarChar<>))
                     && paramType.GetGenericArguments().Single().GetGenericArguments().Single().GetLength() ==
                        sqlParamInfo.GetNVarCharLength(xmlDocument, xmlNamespaceManager))
            {
                //no-op
            }
            else
            {
                throw new QueryCheckException(
                    string.Format(
                        @"The type of parameter '{0}' was incorrect. Expected SQL Server Database Engine type DataType = '{1}', Length = '{5}' but was .NET Framework type '{2}'.
QueryType = '{4}'
QueryText = 
{3}",
                        sqlParamInfo.ParamInfo.Name,
                        sqlParamInfo.SqlType,
                        paramType,
                        queryText,
                        queryType,
                        sqlParamInfo.GetNVarCharLength(xmlDocument, xmlNamespaceManager)));
            }
            Console.WriteLine("{0} {1} {2} {3}",
                              sqlParamInfo.ParamInfo.Name,
                              sqlParamInfo.ParamInfo.ParamType,
                              sqlParamInfo.SqlType,
                              sqlParamInfo.GetNVarCharLength(xmlDocument, xmlNamespaceManager));
        }

        private static void CheckParameterType<T>(SqlParamInfo sqlParamInfo, string queryText, Type queryType)
        {
            var paramType = sqlParamInfo.ParamInfo.ParamType;
            if (typeof(T).Equals(paramType))
            {
                //no-op
            }
            else if (typeof(IOptionalValue<T>).Equals(paramType))
            {
                //no-op
            }
            else
            {
                throw new QueryCheckException(
                    string.Format(
                        @"The type of parameter '{0}' was incorrect. Expected SQL Server Database Engine type DataType = '{1}' but was .NET Framework type '{2}'.
QueryType = '{4}'
QueryText = 
{3}",
                        sqlParamInfo.ParamInfo.Name,
                        sqlParamInfo.SqlType,
                        paramType,
                        queryText,
                        queryType));
            }
            Console.WriteLine("{0} {1} {2}",
                              sqlParamInfo.ParamInfo.Name,
                              sqlParamInfo.ParamInfo.ParamType,
                              sqlParamInfo.SqlType);
        }

        public static QueryResultPropertyTypeChecker QueryResultPropertyTypeChecker
        {
            get
            {
                return (propertyInfo, dataRow, queryTypeFunc) =>
                           {
                               if (dataRow.GetDataTypeName().CompareSqlTypes("nvarchar"))
                               {
                                   CheckNVarCharPropertyType(propertyInfo, dataRow, queryTypeFunc);
                               }
                               else if (dataRow.GetDataTypeName().CompareSqlTypes("decimal"))
                               {
                                   CheckDecimalPropertyType(propertyInfo, dataRow, queryTypeFunc);
                               }
                               else if (dataRow.GetDataTypeName().CompareSqlTypes("money"))
                               {
                                   CheckPropertyType<Money>(propertyInfo, dataRow, queryTypeFunc);
                               }
                               else if (dataRow.GetDataTypeName().CompareSqlTypes("uniqueidentifier"))
                               {
                                   CheckPropertyType<Guid>(propertyInfo, dataRow, queryTypeFunc);
                               }
                               else if (dataRow.GetDataTypeName().CompareSqlTypes("int"))
                               {
                                   CheckPropertyType<int>(propertyInfo, dataRow, queryTypeFunc);
                               }
                               else
                               {
                                   throw new InvalidOperationException();
                               }
                           };
            }
        }

        private static void CheckDecimalPropertyType(PropertyInfo propertyInfo, DataRow dataRow, Func<Type> queryTypeFunc)
        {
            var propertyType = propertyInfo.PropertyType;
            if (propertyType.IsGenericTypeAndEquals(typeof (Decimal<,>))
                && dataRow.GetAllowDbNull() == false
                && propertyType.GetGenericArguments()[0].GetPrecision() == dataRow.GetNumericPrecision()
                && propertyType.GetGenericArguments()[1].GetScale() == dataRow.GetNumericScale())
            {
                //no-op
            }
            else if (propertyType.IsGenericTypeAndEquals(typeof (IOptionalValue<>))
                     && propertyType.GetGenericArguments().Single().IsGenericTypeAndEquals(typeof (Decimal<,>))
                     && dataRow.GetAllowDbNull() == true
                     && propertyType.GetGenericArguments().Single().GetGenericArguments()[0].GetPrecision() == dataRow.GetNumericPrecision()
                     && propertyType.GetGenericArguments().Single().GetGenericArguments()[1].GetScale() == dataRow.GetNumericScale())
            {
                //no-op
            }
            else
            {
                throw new QueryCheckException(
                    string.Format(
                        @"The type of query result property was incorrect ColumnName = '{0}'. Expected SQL Server Database Engine type DataType = '{1}', Precision = '{5}', Scale = '{6}', AllowDBNull = '{2}' but was .NET Framework type '{3}'.
QueryType = '{4}'",
                        dataRow.GetColumnName(),
                        dataRow.GetDataTypeName(),
                        dataRow.GetAllowDbNull(),
                        propertyType,
                        queryTypeFunc(),
                        dataRow.GetNumericPrecision(),
                        dataRow.GetNumericScale()));

            }
            Console.WriteLine(@"{0} {1} '{4}' '{5}' '{2}' {3}",
                              dataRow.GetColumnName(),
                              dataRow.GetDataType(),
                              dataRow.GetAllowDbNull(),
                              propertyType,
                              dataRow.GetNumericPrecision(),
                              dataRow.GetNumericScale());
        }

        private static void CheckNVarCharPropertyType(PropertyInfo propertyInfo, DataRow dataRow, Func<Type> queryTypeFunc)
        {
            var propertyType = propertyInfo.PropertyType;
            if (propertyType.IsGenericTypeAndEquals(typeof(NVarChar<>))
                && dataRow.GetAllowDbNull() == false
                && propertyType.GetGenericArguments().Single().GetLength() == dataRow.GetColumnSize())
            {
                //no-op
            }
            else
            {
                if (propertyType.IsGenericTypeAndEquals(typeof(IOptionalValue<>))
                    && propertyType.GetGenericArguments().Single().IsGenericTypeAndEquals(typeof(NVarChar<>))
                    && dataRow.GetAllowDbNull() == true
                    && propertyType.GetGenericArguments().Single().GetGenericArguments().Single().GetLength() == dataRow.GetColumnSize())
                {
                    //no-op
                }
                else
                {
                    throw new QueryCheckException(
                        string.Format(
                            @"The type of query result property was incorrect ColumnName = '{0}'. Expected SQL Server Database Engine type DataType = '{1}', ColumnSize = '{5}', AllowDBNull = '{2}' but was .NET Framework type '{3}'.
QueryType = '{4}'",
                            dataRow.GetColumnName(),
                            dataRow.GetDataTypeName(),
                            dataRow.GetAllowDbNull(),
                            propertyType,
                            queryTypeFunc(),
                            dataRow.GetColumnSize()));
                }
            }
            Console.WriteLine(@"{0} {1} '{4}' '{2}' {3}",
                              dataRow.GetColumnName(),
                              dataRow.GetDataType(),
                              dataRow.GetAllowDbNull(),
                              propertyType,
                              dataRow.GetColumnSize());
        }

        private static void CheckPropertyType<T>(PropertyInfo propertyInfo, DataRow dataRow, Func<Type> queryTypeFunc)
        {
            var propertyType = propertyInfo.PropertyType;
            if (typeof(T).Equals(propertyType) && dataRow.GetAllowDbNull() == false)
            {
                //no-op
            }
            else if (typeof(IOptionalValue<T>).Equals(propertyType) && dataRow.GetAllowDbNull() == true)
            {
                //no-op
            }
            else
            {
                throw new QueryCheckException(
                    string.Format(
                        @"The type of query result property was incorrect ColumnName = '{0}'. Expected SQL Server Database Engine type DataType = '{1}', AllowDBNull = '{2}' but was .NET Framework type '{3}'.
QueryType = '{4}'",
                        dataRow.GetColumnName(),
                        dataRow.GetDataTypeName(),
                        dataRow.GetAllowDbNull(),
                        propertyType,
                        queryTypeFunc()));
            }
            Console.WriteLine(@"{0} {1} '{2}' {3}",
                              dataRow.GetColumnName(),
                              dataRow.GetDataType(),
                              dataRow.GetAllowDbNull(),
                              propertyType);
        }

        private static string ConnectionString
        {
            get { return @"Data Source=(local);Initial Catalog=ControllableQuery;Integrated Security=True"; }
        }
    }
}